Popular Searches
Popular Course Categories
Popular Courses

Optimizing FlatList and Large Data Collections

Optimizing FlatList and Large Data Collections

Performance Optimization and Memory Management

Optimizing FlatList and Large Data Collections in React Native

Course: React Native Training

Topic: Optimizing FlatList and Large Data Collections

Course Link: React Native Training at JustAcademy

Demo Registration Link: Register for a Free Course Demo

JustAcademy Website: Visit JustAcademy


1. Introduction to FlatList Optimization

React Native applications often display collections of data such as product catalogs, contact lists, chat messages, transaction histories, notifications, and social media feeds. When these collections contain hundreds or thousands of records, rendering every item at once can increase memory consumption, slow down scrolling, and reduce application responsiveness.

FlatList is a built-in React Native component designed to display scrollable lists efficiently. It renders items lazily and maintains a limited rendering window rather than requiring every item to remain mounted simultaneously. However, a poorly configured FlatList can still cause unnecessary re-renders, blank areas during fast scrolling, excessive memory usage, or sluggish interactions.

Optimizing FlatList involves selecting appropriate rendering settings, writing efficient row components, using stable keys, minimizing expensive calculations, managing image loading, and fetching large datasets in manageable portions.

Learning Objectives

  • Understand how FlatList renders and virtualizes list items.
  • Differentiate FlatList from ScrollView and other list components.
  • Configure important FlatList performance properties.
  • Reduce unnecessary rendering with React.memo and stable callbacks.
  • Implement pagination, infinite scrolling, and efficient data updates.
  • Optimize images and complex list-item layouts.
  • Measure performance and troubleshoot common list-rendering problems.

2. Understanding FlatList

FlatList is a React Native component that renders a collection of data through a reusable item-rendering function. It is particularly suitable for long, changing, or remotely loaded collections.

2.1 Basic Syntax

import React from 'react';
import { FlatList, Text, View } from 'react-native';

const DATA = [
  { id: '1', title: 'React Native' },
  { id: '2', title: 'JavaScript' },
  { id: '3', title: 'TypeScript' },
];

function Item({ title }) {
  return (
    <View>
      <Text>{title}</Text>
    </View>
  );
}

export default function App() {
  return (
    <FlatList
      data={DATA}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <Item title={item.title} />
      )}
    />
  );
}

2.2 Important FlatList Properties

Property Purpose Optimization Benefit
data Provides the array of list records. Use predictable, immutable data updates.
renderItem Defines how each item is displayed. Keep rendering logic lightweight.
keyExtractor Returns a unique key for each item. Helps React identify items consistently.
initialNumToRender Controls the initial number of items rendered. Avoids rendering too many items at startup.
maxToRenderPerBatch Controls the number of items rendered in each incremental batch. Balances fill rate and responsiveness.
updateCellsBatchingPeriod Controls the interval between incremental render batches, in milliseconds. Helps balance rendering work and responsiveness.
windowSize Controls the size of the rendering window in viewport units. Balances memory use and scrolling smoothness.
getItemLayout Provides item dimensions and offsets when known. Can avoid runtime measurement for fixed-size rows.
extraData Identifies external data that affects rendered rows. Supports correct updates when dependencies change.
onEndReached Runs when scrolling approaches the end of the list. Supports pagination and infinite scrolling.
removeClippedSubviews Allows offscreen child views to be detached from the native view hierarchy in supported situations. May reduce native view work, but requires testing.

3. FlatList vs ScrollView

Choosing the correct component is the first step toward efficient collection rendering.

Feature FlatList ScrollView
Rendering strategy Virtualized rendering window. Normally renders all its children.
Large collections Generally preferred. Can become expensive as the collection grows.
Item recycling or virtualization Maintains a limited active rendering window. No built-in list virtualization.
Best use cases Product feeds, contacts, messages, and long lists. Short forms, simple screens, and small static content.
Pagination support Provides callbacks useful for loading additional data. Requires custom scroll handling.

Best practice: Use FlatList for large or potentially growing collections. Use ScrollView when the content is small and rendering all children is reasonable.

4. How FlatList Virtualization Works

Virtualization means that the list maintains a limited set of active items around the visible viewport. As users scroll, the rendering window moves and items outside that window may be unmounted or otherwise removed from active rendering. Some initial items may be retained for smoother scrolling back toward the beginning.

4.1 Benefits of Virtualization

  • Reduces the number of simultaneously mounted row components.
  • Helps control memory and native-view overhead.
  • Allows long collections to scroll more efficiently than rendering every row at once.
  • Supports lists that receive additional data over time.

4.2 Virtualization Limitations

  • Rows may appear blank briefly if the user scrolls faster than the list can render new items.
  • Rows that leave the rendering window may lose local component state.
  • Expensive row components can still cause frame drops.
  • Very large datasets stored in memory still consume JavaScript memory even when only some rows are rendered.

Do not assume that FlatList automatically solves every performance problem. The size of the dataset, complexity of each row, image processing, state updates, and network operations all affect the final result.

5. Use Stable and Unique Keys

Each rendered item should have a stable, unique key. Keys help React distinguish items when records are inserted, removed, reordered, or updated.

5.1 Recommended Approach

<FlatList
  data={products}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => (
    <ProductCard product={item} />
  )}
/>

Use a unique identifier from the data source, such as a database ID or product ID. The identifier should remain the same for the same logical record across renders.

5.2 Avoid Array Indexes for Changing Lists

// Avoid for lists where items can be inserted, removed, or reordered.
keyExtractor={(item, index) => String(index)}

Index-based keys can cause incorrect component identity when the order changes. They may be acceptable for genuinely static lists whose items never change order or membership, but stable record IDs are usually safer.

6. Keep renderItem Lightweight

The renderItem function may execute repeatedly as list data, state, or rendering conditions change. It should avoid expensive computations, unnecessary object creation, and complex business logic whenever possible.

6.1 Move Complex UI into a Dedicated Component

import React, { memo } from 'react';
import { View, Text } from 'react-native';

const ProductRow = memo(function ProductRow({ product }) {
  return (
    <View>
      <Text>{product.name}</Text>
      <Text>${product.price.toFixed(2)}</Text>
    </View>
  );
});

function ProductList({ products }) {
  return (
    <FlatList
      data={products}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <ProductRow product={item} />
      )}
    />
  );
}

In this example, ProductRow is separated from the list configuration. Wrapping it with memo can skip some re-renders when its props remain shallowly equal. It does not prevent rendering when relevant props change, and it does not automatically optimize every part of the parent list.

6.2 Avoid Expensive Work During Rendering

Do not repeatedly sort, filter, parse large datasets, or calculate complex values inside each row's render path unless needed. Perform expensive transformations outside the row component, cache them when appropriate, or prepare the required display values when the data is received.

// Less efficient when repeated for every row:
const total = products.reduce(
  (sum, product) => sum + product.price,
  0
);

// Better: calculate aggregate values once where needed,
// rather than recalculating the full collection for each row.

7. Tune initialNumToRender

initialNumToRender specifies how many items FlatList initially renders. A reasonable value should fill the first viewport without creating a large amount of unnecessary startup work.

<FlatList
  data={products}
  renderItem={renderProduct}
  keyExtractor={(item) => item.id}
  initialNumToRender={10}
/>

The value of 10 is only an example, not a universal recommendation. A list with tall cards may need fewer items to fill the viewport, while a list with compact rows may need more.

Guidelines

  • Estimate how many rows fit on the initial screen.
  • Start with enough rows to cover the viewport and a small buffer if needed.
  • Measure startup time and initial blank space on real devices.
  • Avoid setting an unnecessarily high value for very complex rows.

8. Optimize maxToRenderPerBatch and updateCellsBatchingPeriod

FlatList renders additional items in batches as the user scrolls. The batch size and interval influence how quickly the list fills its rendering window and how much work competes with user interactions.

8.1 Example Configuration

<FlatList
  data={products}
  renderItem={renderProduct}
  keyExtractor={(item) => item.id}
  maxToRenderPerBatch={8}
  updateCellsBatchingPeriod={50}
/>

These values are starting points for experimentation. A larger batch may fill blank space faster but can block interaction while more rows render. Smaller batches may improve responsiveness but can reveal blank areas during fast scrolling. A shorter batching period triggers batches more frequently; the best balance depends on row complexity and device performance.

8.2 Tuning Strategy

  1. Test the default configuration first.
  2. Reproduce fast scrolling on a lower-end device.
  3. Increase batch size if blank regions appear frequently and rendering is otherwise lightweight.
  4. Reduce batch size if scrolling or touch interactions become noticeably unresponsive.
  5. Adjust the batching period and repeat the same tests.
  6. Keep the settings that improve measured performance without creating new problems.

9. Optimize windowSize

windowSize controls how much content around the viewport is kept in the active rendering window. The default is commonly 21 viewport lengths, including the visible viewport and regions before and after it; confirm the behavior for your React Native version.

<FlatList
  data={products}
  renderItem={renderProduct}
  keyExtractor={(item) => item.id}
  windowSize={9}
/>

A smaller window can reduce the number of active rows and lower memory or rendering overhead, but it increases the risk of blank areas during fast scrolling. A larger window can make upcoming items ready sooner but requires more resources.

Best Practices

  • Use a smaller window only when memory or rendering cost is a demonstrated concern.
  • Test with realistic row heights and fast scrolling.
  • Check both memory use and visual continuity.
  • Avoid changing several FlatList properties at the same time because it becomes harder to identify which change helped.

10. Use getItemLayout for Fixed-Size Rows

getItemLayout allows FlatList to determine item dimensions and positions without measuring each row dynamically. This is especially useful for lists containing hundreds or thousands of rows with predictable heights.

10.1 Example with Fixed Row Height

import React from 'react';
import { FlatList, Text, View } from 'react-native';

const ITEM_HEIGHT = 60;

const DATA = Array.from({ length: 1000 }, (_, index) => ({
  id: String(index + 1),
  title: `Item ${index + 1}`,
}));

export default function LargeList() {
  const renderItem = ({ item }) => (
    <View style={{ height: ITEM_HEIGHT, justifyContent: 'center' }}>
      <Text>{item.title}</Text>
    </View>
  );

  return (
    <FlatList
      data={DATA}
      keyExtractor={(item) => item.id}
      renderItem={renderItem}
      getItemLayout={(_, index) => ({
        length: ITEM_HEIGHT,
        offset: ITEM_HEIGHT * index,
        index,
      })}
    />
  );
}

10.2 Including Separator Height

If every row is 60 units high and each separator adds 1 unit, the total interval between row starts is 61 units. The layout calculation must account for the separator.

const ITEM_HEIGHT = 60;
const SEPARATOR_HEIGHT = 1;
const ITEM_INTERVAL = ITEM_HEIGHT + SEPARATOR_HEIGHT;

getItemLayout={(_, index) => ({
  length: ITEM_INTERVAL,
  offset: ITEM_INTERVAL * index,
  index,
})}

Make sure the returned length and offset match the actual layout. Incorrect measurements can cause inaccurate scrolling and positioning. For dynamic-height rows, do not provide guessed fixed dimensions.

11. Use React.memo to Prevent Unnecessary Row Renders

React.memo can skip re-rendering a row when its props are shallowly equal to the previous props. It is most useful when rows are relatively expensive to render and most item props remain unchanged.

import React, { memo } from 'react';
import { Pressable, Text, View } from 'react-native';

const ContactRow = memo(function ContactRow({ contact, onSelect }) {
  return (
    <Pressable onPress={() => onSelect(contact.id)}>
      <View>
        <Text>{contact.name}</Text>
        <Text>{contact.email}</Text>
      </View>
    </Pressable>
  );
});

Benefits

  • Reduces unnecessary rendering of unchanged row components.
  • Can improve responsiveness when only a small portion of the list changes.
  • Works well with stable object references and stable callback props.

Important Considerations

  • Creating a new object for a row's props on every render can defeat shallow comparison.
  • Functions passed as props can also change identity on each render.
  • Memoization adds comparison work, so it should be used when it provides a measurable benefit.
  • Do not write custom comparison functions that ignore props affecting visible output or behavior.

12. Use useCallback and useMemo Correctly

useCallback can preserve a function reference between renders while its dependencies remain unchanged. useMemo can cache a calculated value. Both hooks can help reduce unnecessary work when used for a specific performance reason.

12.1 Stable renderItem Callback

import React, { useCallback } from 'react';
import { FlatList } from 'react-native';

function ProductList({ products, onProductPress }) {
  const renderItem = useCallback(
    ({ item }) => (
      <ProductRow
        product={item}
        onSelect={onProductPress}
      />
    ),
    [onProductPress]
  );

  return (
    <FlatList
      data={products}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
    />
  );
}

The callback depends on onProductPress, so that dependency must be included. If the parent creates a new callback on every render, the reference may still change. The parent can use useCallback when keeping that reference stable is useful.

12.2 Memoize Expensive Derived Data

import React, { useMemo } from 'react';

function ProductScreen({ products, searchText }) {
  const filteredProducts = useMemo(() => {
    const query = searchText.trim().toLowerCase();

    return products.filter((product) =>
      product.name.toLowerCase().includes(query)
    );
  }, [products, searchText]);

  return (
    <ProductList products={filteredProducts} />
  );
}

This example caches the filtered array while its dependencies remain unchanged. It does not make filtering free, and it is most useful when filtering is sufficiently expensive or the inputs often remain stable. Avoid adding hooks everywhere without measuring their benefit.

13. Handle External State with extraData

FlatList uses shallow prop comparison. If a row depends on a value outside the data array, FlatList may not update as expected unless a relevant prop changes. The extraData property can tell FlatList that an external value affects its rendered output.

Example: Selecting a Row

import React, { useState } from 'react';
import { FlatList, Pressable, Text } from 'react-native';

export default function SelectableList({ items }) {
  const [selectedId, setSelectedId] = useState(null);

  const renderItem = ({ item }) => (
    <Pressable
      onPress={() => setSelectedId(item.id)}
    >
      <Text>
        {selectedId === item.id ? '✓ ' : ''}
        {item.title}
      </Text>
    </Pressable>
  );

  return (
    <FlatList
      data={items}
      extraData={selectedId}
      keyExtractor={(item) => item.id}
      renderItem={renderItem}
    />
  );
}

Here, selectedId changes independently of the data array. Providing it through extraData makes the dependency explicit. Continue to update state immutably and avoid mutating the original data array.

14. Optimize Large Data Collections

Virtualization limits how many rows are actively rendered, but it does not eliminate the cost of storing a very large array in JavaScript memory. Large datasets require appropriate data loading, filtering, updating, and caching strategies.

14.1 Use Pagination

Pagination divides a large collection into smaller pages. Instead of requesting thousands of records at once, the application requests a manageable batch and loads subsequent batches when needed.

  • Reduce initial network payload size.
  • Show useful content sooner.
  • Limit the amount of data retained in memory.
  • Make retries and error recovery easier to manage.

14.2 Implement Infinite Scrolling

The onEndReached callback can trigger a request for the next page as the user approaches the end of the list. Prevent duplicate requests by tracking loading state, and use a stable pagination cursor or page number from the API.

import React, { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, FlatList, Text, View } from 'react-native';

const PAGE_SIZE = 20;

export default function PaginatedList() {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const [error, setError] = useState('');

  const loadPage = useCallback(async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    setError('');

    try {
      // Replace this with your real API request.
      const response = await fetch(
        `https://example.com/api/items?page=${page}&limit=${PAGE_SIZE}`
      );

      if (!response.ok) {
        throw new Error('Unable to load items.');
      }

      const result = await response.json();
      const newItems = result.items ?? [];

      setItems((previous) => {
        const existingIds = new Set(previous.map((item) => item.id));
        const uniqueNewItems = newItems.filter(
          (item) => !existingIds.has(item.id)
        );
        return [...previous, ...uniqueNewItems];
      });

      setHasMore(
        typeof result.hasMore === 'boolean'
          ? result.hasMore
          : newItems.length === PAGE_SIZE
      );

      setPage((previous) => previous + 1);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Something went wrong.');
    } finally {
      setLoading(false);
    }
  }, [page, loading, hasMore]);

  useEffect(() => {
    loadPage();
  }, [loadPage]);

  const renderItem = ({ item }) => (
    <View>
      <Text>{item.title}</Text>
    </View>
  );

  return (
    <View style={{ flex: 1 }}>
      {error ? <Text>{error}</Text> : null}
      <FlatList
        data={items}
        keyExtractor={(item) => item.id}
        renderItem={renderItem}
        onEndReached={loadPage}
        onEndReachedThreshold={0.5}
        ListFooterComponent={
          loading ? <ActivityIndicator /> : null
        }
      />
    </View>
  );
}

Example limitations: This code assumes the API returns an object with an items array and optionally a hasMore boolean. Replace the sample URL and response parsing with your API's actual contract. The loading state reduces duplicate requests, but state updates are asynchronous, and closely spaced callbacks can still race in some implementations. For production use, consider an in-flight request guard held in a ref, cancellation on unmount or query changes, and request identifiers to prevent stale responses from being appended.

14.3 Additional Pagination Best Practices

  • Prefer cursor-based pagination when the backend supports it and records change frequently.
  • Debounce or cancel search requests when users type quickly.
  • Reset the page or cursor when filters or sort order change.
  • Deduplicate records using stable IDs.
  • Display loading, empty, and error states.
  • Avoid repeatedly requesting the same final page when no more data exists.
  • Consider limiting retained pages for extremely long sessions, while preserving scroll position and user expectations.

15. Efficiently Update List Data

React relies on references and state updates to determine when components should render. Mutating an existing array or item object can prevent expected updates and create confusing behavior.

15.1 Add an Item Immutably

setItems((previousItems) => [
  ...previousItems,
  newItem,
]);

15.2 Update a Single Item

setItems((previousItems) =>
  previousItems.map((item) =>
    item.id === updatedItem.id
      ? { ...item, ...updatedItem }
      : item
  )
);

15.3 Remove an Item

setItems((previousItems) =>
  previousItems.filter((item) => item.id !== itemId)
);

These patterns create a new array while preserving unchanged item references. This can work well with memoized rows because unaffected records may retain their references.

16. Optimize Images in List Items

Product catalogs, user profiles, and social feeds often contain many images. Large image files, frequent decoding, and repeated image loading can make scrolling slower and increase memory pressure.

Best Practices

  • Request thumbnails or appropriately sized image variants rather than downloading full-resolution images for small list cards.
  • Use stable image URLs and an image-caching solution appropriate to your React Native version and application architecture.
  • Set predictable image dimensions to reduce layout shifts.
  • Provide a placeholder or fallback for failed image requests.
  • Avoid unnecessary image transformations during each render.
  • Test image-heavy lists on physical devices with limited memory and slower connections.

Example

import React, { memo } from 'react';
import { Image, Text, View } from 'react-native';

const ProductImageRow = memo(function ProductImageRow({ product }) {
  return (
    <View>
      <Image
        source={{ uri: product.thumbnailUrl }}
        style={{ width: 72, height: 72 }}
        resizeMode="cover"
      />
      <Text>{product.name}</Text>
    </View>
  );
});

React Native's built-in Image component supports common image-loading behavior, but advanced caching and request management may require a suitable library. Check current maintenance, compatibility, and platform support before adding a third-party dependency.

17. Simplify Row Layouts

Every row adds rendering and layout work. Deeply nested views, many shadows, complex conditional layouts, and large numbers of interactive children can increase the cost of rendering a list.

  • Use simple layouts for frequently repeated rows.
  • Avoid deeply nested view hierarchies when a simpler structure works.
  • Keep formatting and calculations outside the rendering path where practical.
  • Use lightweight separators rather than complex decorative components.
  • Render expensive secondary content only when necessary.
  • Ensure touch targets remain usable even when simplifying the layout.

18. Use removeClippedSubviews Carefully

The removeClippedSubviews property can detach certain offscreen views from the native view hierarchy. Depending on the platform and layout, this may reduce native drawing work.

<FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  removeClippedSubviews={true}
/>

Test this option on both Android and iOS before using it in production. It can cause missing-content or positioning issues with some combinations of transforms, absolute positioning, and complex layouts. It does not necessarily free the JavaScript data or deallocate all offscreen view-related memory.

19. Add Headers, Footers, and Empty States Efficiently

List headers and footers are useful for search controls, category labels, loading indicators, and end-of-list messages. Keep these components lightweight and avoid recreating complex header elements unnecessarily.

<FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  ListHeaderComponent={ListHeader}
  ListEmptyComponent={EmptyState}
  ListFooterComponent={loading ? LoadingFooter : null}
/>

Define reusable header, footer, and empty-state components outside the parent render function when appropriate. If they depend on changing values, pass those values explicitly and make sure FlatList receives updated props when needed.

20. Search, Filtering, and Sorting for Large Collections

Search and filtering can become expensive when applied to very large arrays on every keystroke. Use an appropriate strategy for the size of the collection and the complexity of the query.

Recommendations

  • Normalize searchable values when data is received if that avoids repeated processing.
  • Use memoization for expensive derived arrays when it provides a measured benefit.
  • Debounce user-entered search terms to avoid excessive filtering or network requests.
  • Move filtering, sorting, and pagination to the backend when the collection is very large.
  • Preserve stable record IDs when the displayed order changes.
  • Show clear feedback when no records match the search.

21. Common FlatList Performance Problems and Solutions

Problem Possible Cause Recommended Solution
Scrolling is choppy Complex rows, expensive calculations, large render batches, or image decoding. Simplify row components, optimize images, profile performance, and tune batching settings.
Blank areas appear during fast scrolling The list cannot render items quickly enough to fill the window. Review row complexity, batch size, batching period, and window size.
Rows render too often Unstable props, changed callbacks, or broad parent state updates. Use React.memo where helpful and keep relevant references stable.
State changes do not appear in rows Mutated data or an external dependency not represented in FlatList props. Use immutable updates and pass external dependencies through props or extraData.
Scrolling to an index is slow Rows have unknown dimensions and require layout measurement. Use getItemLayout when row dimensions are predictable.
High memory consumption Large retained datasets, expensive rows, large images, or a large rendering window. Paginate data, resize images, simplify rows, and test a smaller window.
Duplicate page requests Repeated end-reached events or asynchronous loading-state races. Use an in-flight guard, request identifiers, and explicit pagination state.
Rows display the wrong local state after reordering Unstable or index-based keys. Use stable unique IDs from the data source.
Content disappears on some devices Clipping behavior interacting with a complex layout. Test removeClippedSubviews settings and simplify problematic positioning.

22. Performance Testing and Profiling

Optimization should be based on measurements rather than assumptions. A list that performs well on a powerful development computer may behave differently on an entry-level phone.

22.1 What to Measure

  • Initial list render time.
  • Scrolling smoothness and dropped frames.
  • Time taken to render newly visible rows.
  • Memory use during long scrolling sessions.
  • Time taken to load the next page.
  • Number of row renders during common interactions.
  • Responsiveness of taps, navigation, and text input while scrolling.

22.2 Testing Checklist

  1. Prepare a realistic dataset with hundreds or thousands of records.
  2. Test on a physical device, including a lower-end device if available.
  3. Use a release or production-like build when measuring actual performance.
  4. Scroll slowly and rapidly through the entire list.
  5. Test image loading, search, filters, selection, and pagination.
  6. Inspect React component rendering and platform performance tools.
  7. Change one configuration setting at a time and compare results.
  8. Repeat tests after adding new row features.

Development mode and debugging tools can add overhead, so avoid relying only on development-build results when making final performance decisions.

23. Complete Example: Optimized Product List

The following example combines memoized row components, stable IDs, fixed row dimensions, pagination-friendly list configuration, and an end-of-list loading indicator. It uses local sample data to keep the example self-contained.

import React, { memo, useCallback, useState } from 'react';
import {
  ActivityIndicator,
  FlatList,
  Pressable,
  Text,
  View,
} from 'react-native';

const ROW_HEIGHT = 64;

const INITIAL_PRODUCTS = Array.from({ length: 1000 }, (_, index) => ({
  id: `product-${index + 1}`,
  name: `Product ${index + 1}`,
  price: ((index + 1) * 2.5).toFixed(2),
}));

const ProductRow = memo(function ProductRow({ item, onSelect }) {
  return (
    <Pressable
      onPress={() => onSelect(item.id)}
      style={{
        height: ROW_HEIGHT,
        justifyContent: 'center',
        paddingHorizontal: 16,
      }}
    >
      <Text>{item.name}</Text>
      <Text>${item.price}</Text>
    </Pressable>
  );
});

export default function OptimizedProductList() {
  const [products] = useState(INITIAL_PRODUCTS);
  const [selectedId, setSelectedId] = useState(null);

  const handleSelect = useCallback((id) => {
    setSelectedId(id);
  }, []);

  const renderItem = useCallback(
    ({ item }) => (
      <ProductRow
        item={item}
        onSelect={handleSelect}
      />
    ),
    [handleSelect]
  );

  const renderSeparator = useCallback(
    () => <View style={{ height: 1 }} />,
    []
  );

  return (
    <FlatList
      data={products}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
      getItemLayout={(_, index) => ({
        length: ROW_HEIGHT + 1,
        offset: (ROW_HEIGHT + 1) * index,
        index,
      })}
      ItemSeparatorComponent={renderSeparator}
      initialNumToRender={10}
      maxToRenderPerBatch={8}
      updateCellsBatchingPeriod={50}
      windowSize={11}
      extraData={selectedId}
      ListFooterComponent={<ActivityIndicator />}
    />
  );
}

Example notes: This example assumes each row is exactly 64 units high and each separator is 1 unit high. The selected ID is passed through extraData to make the external dependency explicit. For a real application, conditionally render the selected state in the row and replace the permanently displayed footer indicator with a real loading condition. The sample array is created once at module scope, so it does not simulate API pagination.

24. Practical Project: Build a High-Performance Product Catalog

Project Objective: Build a React Native screen that displays a large product catalog without unnecessary rendering or excessive memory use.

Project Requirements

  1. Create a product data model with a stable ID, name, price, category, and thumbnail URL.
  2. Display products using FlatList.
  3. Build a reusable, lightweight ProductRow component.
  4. Use stable keys and apply React.memo when beneficial.
  5. Implement search and category filtering.
  6. Load products using API pagination or an equivalent simulated data source.
  7. Show loading, empty, and error states.
  8. Optimize fixed-size rows with getItemLayout when dimensions are predictable.
  9. Measure scrolling performance with realistic amounts of data.
  10. Document the configuration changes that produced measurable improvements.

Expected Outcome

The completed application should load its initial content efficiently, render long lists without excessive work, avoid duplicate pagination requests, display consistent row state, and remain responsive while users search, scroll, and select products.

25. Best Practices Summary

  • Choose FlatList rather than ScrollView for large or growing lists.
  • Use stable, unique item keys.
  • Keep row components simple and lightweight.
  • Use React.memo, useCallback, and useMemo only where they help.
  • Set initialNumToRender based on actual viewport needs.
  • Tune windowSize and batch settings using real-device tests.
  • Use getItemLayout for accurately known fixed dimensions.
  • Use immutable updates and extraData for external rendering dependencies.
  • Paginate large collections and guard against duplicate requests.
  • Resize and cache images appropriately.
  • Test removeClippedSubviews carefully on all target platforms.
  • Profile before and after optimization instead of guessing.

26. Interview Questions and Answers

Q1. What is FlatList in React Native?

Answer: FlatList is a component for rendering scrollable data collections with virtualization. It maintains a rendering window so that the entire collection does not need to be mounted at once.

Q2. Why is FlatList usually better than ScrollView for large datasets?

Answer: ScrollView normally renders all its children, while FlatList renders a limited window of items. This generally makes FlatList more suitable for long lists.

Q3. What is the purpose of keyExtractor?

Answer: It provides a unique key for each item, helping React identify records consistently when the collection changes.

Q4. What does getItemLayout do?

Answer: It supplies known item dimensions and offsets so FlatList can avoid dynamic measurement. It is useful for fixed-size rows and requires accurate calculations.

Q5. How does React.memo help FlatList performance?

Answer: It can prevent a row component from re-rendering when its props remain shallowly equal. It is not a guarantee that all row renders will be skipped.

Q6. What is the difference between useCallback and useMemo?

Answer: useCallback caches a function reference, while useMemo caches the result of a calculation. Both depend on correct dependency lists.

Q7. What does windowSize control?

Answer: It controls the approximate size of the active rendering window around the visible viewport. Smaller values may reduce resource use but can increase blank areas during rapid scrolling.

Q8. What is extraData used for?

Answer: It tells FlatList that an external value affects list rendering, allowing the list to update when that value changes even if the data array reference has not changed.

Q9. How do you implement infinite scrolling?

Answer: Use onEndReached to request another page, track loading and pagination state, avoid duplicate requests, append unique records, and stop loading when no more data exists.

Q10. Why can a FlatList still scroll poorly?

Answer: Virtualization does not eliminate expensive row rendering, image decoding, large JavaScript data transformations, excessive state updates, or poor batching settings.

Q11. What is the risk of using array indexes as keys?

Answer: When items are inserted, removed, or reordered, indexes may point to different records. This can cause component identity and local state to become associated with the wrong item.

Q12. How should FlatList performance be tested?

Answer: Test realistic datasets on physical devices, profile release-like builds, inspect render counts and frame performance, and compare one optimization change at a time.

27. Quick Revision Checklist

  • Understand FlatList virtualization.
  • Use stable IDs through keyExtractor.
  • Keep renderItem and row components lightweight.
  • Apply memoization only when appropriate.
  • Configure initialNumToRender, maxToRenderPerBatch, updateCellsBatchingPeriod, and windowSize based on measurements.
  • Use getItemLayout only when dimensions are known accurately.
  • Pass external state through props or extraData.
  • Use immutable data updates.
  • Implement robust pagination and loading guards.
  • Optimize image sizes and caching.
  • Profile scrolling, memory, and interaction responsiveness.

28. Official Learning Resources

29. Conclusion

Optimizing FlatList and large data collections is an important skill for building responsive React Native applications. Strong results come from combining virtualization, stable keys, efficient row components, accurate layout calculations, carefully tuned rendering batches, image optimization, and well-designed pagination.

There is no single configuration that works best for every application. Measure the actual list on representative devices, identify the bottleneck, make targeted improvements, and test again. This approach helps deliver smoother scrolling, faster loading, lower resource usage, and a better user experience.

Explore React Native Training: View the React Native Course at JustAcademy

Register for a Demo: Register for a Course Demo

Visit JustAcademy: https://www.justacademy.co/

whatsapp